﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls.Primitives;
using theeg.GeometryWpf;
using theeg.GraphWpf.Models;
using theeg.GraphWpf.Views;
using Node = theeg.GraphWpf.Models.Node;

namespace theeg.GraphWpf
{
    public class Sketch : Control, IGraph
    {
        public event RoutedEventHandler InputDown
        {
            add { AddHandler(PresenterBase.InputDownEvent, value); }
            remove { RemoveHandler(PresenterBase.InputDownEvent, value); }
        }

        public event RoutedEventHandler InputUp
        {
            add { AddHandler(PresenterBase.InputUpEvent, value); }
            remove { RemoveHandler(PresenterBase.InputUpEvent, value); }
        }

        public event RoutedEventHandler InputDouble
        {
            add { AddHandler(PresenterBase.InputDoubleEvent, value); }
            remove { RemoveHandler(PresenterBase.InputDoubleEvent, value); }
        }

        public event RoutedEventHandler SubgraphExpanded
        {
            add { AddHandler(SubGraphPresenter.SubgraphExpandedEvent, value); }
            remove { RemoveHandler(SubGraphPresenter.SubgraphExpandedEvent, value); }
        }

        private Canvas _sketchPanel;
        private Border _viewport;
        private ScrollBar _horizontalScrollBar;
        private ScrollBar _verticalScrollBar;
        private readonly HilbertTree _hilbertTree = HilbertTree.BuildTree(null);
        private double _verticalScrollValue;
        private double _horizontalScrollValue;
        private double _verticalOffset;
        private double _horizontalOffset;
        private const double ScrollbarMinimum = -1;
        private const double ScrollbarMaximum = 1;
        private int _skipUpdateObjectsInScopeCount;
        private MatrixTransform _transformBackup = new MatrixTransform();

        public bool IsExpanded
        {
            get { return true; }
        }

        private readonly IList<INode> _nodes = new List<INode>();

        public IList<INode> Nodes
        {
            get { return _nodes; }
        }

        private readonly IList<Transition> _transitions = new List<Transition>();

        public IList<Transition> Transitions
        {
            get { return _transitions; }
        }

        public double ZoomFactor
        {
            get { return (double)GetValue(ZoomFactorProperty); }
            set { SetValue(ZoomFactorProperty, value); }
        }

        public static readonly DependencyProperty ZoomFactorProperty =
            DependencyProperty.Register("ZoomFactor", typeof(double), typeof(Sketch), new PropertyMetadata(1.0, ZoomFactorChanged));

        public Sketch()
        {
            DefaultStyleKey = typeof(Sketch);

            SubgraphExpanded += (x, y) =>
            {
                Point ptviewcenter = _sketchPanel.PointFromScreen(
                    _viewport.PointToScreen(new Point(_viewport.ActualWidth * 0.5, _viewport.ActualHeight * 0.5)));
                Arrange();
                CenterTo(ptviewcenter.X, ptviewcenter.Y);
            };
        }

        private static void ZoomFactorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Sketch graph = obj as Sketch;
            double newzoom = (double)args.NewValue;
            if (graph._sketchPanel != null && graph._viewport != null)
            {
                graph.UpdateRenderTransform();

                var boundingArea = graph.GetGraphBoundingBox();
                double scrollableWidth = graph.GetScrollableWidth(boundingArea);
                double scrollableHeight = graph.GetScrollableHeight(boundingArea);

                //scroll to current offsets
                graph.EnterSuppressUpdate();
                graph.ScrollToHorizontalOffset(graph._horizontalScrollValue * scrollableWidth * newzoom);
                graph.LeaveSuppressUpdate();
                graph.ScrollToVerticalOffset(graph._verticalScrollValue * scrollableHeight * newzoom);
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _viewport = GetTemplateChild("viewport") as Border;
            _sketchPanel = GetTemplateChild("sketchPanel") as Canvas;
            if (_viewport != null)
            {
                _viewport.SizeChanged += ViewportSizeChanged;
                _viewport.PreviewMouseDown += ViewportPreviewMouseDown;
                _viewport.MouseMove += ViewportMouseMove;
            }
            _horizontalScrollBar = GetTemplateChild("horizontalScrollBar") as ScrollBar;
            _verticalScrollBar = GetTemplateChild("verticalScrollBar") as ScrollBar;
            if (_horizontalScrollBar != null)
            {
                _horizontalScrollBar.Minimum = ScrollbarMinimum;
                _horizontalScrollBar.Maximum = ScrollbarMaximum;
                _horizontalScrollBar.LargeChange = 0.1;
                _horizontalScrollBar.SmallChange = 0.02;
                _horizontalScrollBar.Value = _horizontalScrollValue;
                _horizontalScrollBar.Scroll += ScrollBarHorizontalScroll;
            }
            if (_verticalScrollBar != null)
            {
                _verticalScrollBar.Minimum = ScrollbarMinimum;
                _verticalScrollBar.Maximum = ScrollbarMaximum;
                _verticalScrollBar.LargeChange = 0.1;
                _verticalScrollBar.SmallChange = 0.02;
                _verticalScrollBar.Value = _verticalScrollValue;
                _verticalScrollBar.Scroll += ScrollBarVerticalScroll;
            }
        }

        private void UpdateRenderTransform()
        {
            if (_sketchPanel == null)
            {
                return;
            }
            var boundingArea = GetGraphBoundingBox();

            double w = _sketchPanel.Width = boundingArea.Width * ZoomFactor;
            double h = _sketchPanel.Height = boundingArea.Height * ZoomFactor;

            Matrix renderMatrix = new Matrix
                                      {
                                          M11 = ZoomFactor,
                                          M22 = ZoomFactor,
                                          OffsetX = (w - (boundingArea.L + boundingArea.R) * ZoomFactor) * 0.5,
                                          OffsetY = (h - (boundingArea.B + boundingArea.T) * ZoomFactor) * 0.5
                                      };
            _transformBackup = new MatrixTransform
                {
                    Matrix = renderMatrix
                };
            _sketchPanel.RenderTransform = _transformBackup;
        }

        private void EnterSuppressUpdate()
        {
            _skipUpdateObjectsInScopeCount++;
        }

        private void LeaveSuppressUpdate()
        {
            _skipUpdateObjectsInScopeCount = Math.Max(0, _skipUpdateObjectsInScopeCount - 1);
        }

        private void ViewportSizeChanged(object sender, SizeChangedEventArgs e)
        {
            EnterSuppressUpdate();
            UpdateRenderTransform();
            ScrollToHorizontalOffset(_horizontalOffset);
            LeaveSuppressUpdate();
            ScrollToVerticalOffset(_verticalOffset);
        }

        private void CenterTo(double x, double y)
        {
            if (_viewport != null)
            {
                var boundingArea = GetGraphBoundingBox();
                //scroll horizontally
                double scrollableWidth = GetScrollableWidth(boundingArea);
                double scrollableHeight = GetScrollableHeight(boundingArea);
                if (double.IsNaN(scrollableHeight) || double.IsNaN(scrollableWidth)) return;

                _horizontalScrollValue = Math.Max(-1, Math.Min(1, ((x - boundingArea.L) - 0.5 * (boundingArea.Width)) / scrollableWidth));
                _verticalScrollValue = Math.Max(-1, Math.Min(1, ((y - boundingArea.T) - 0.5 * (boundingArea.Height)) / scrollableHeight));
                if (_horizontalScrollBar != null)
                {
                    _horizontalScrollBar.Value = _horizontalScrollValue;
                }
                if (_verticalScrollBar != null)
                {
                    _verticalScrollBar.Value = _verticalScrollValue;
                }

                EnterSuppressUpdate();
                ScrollToHorizontalOffset(_horizontalScrollValue * scrollableWidth * ZoomFactor);
                LeaveSuppressUpdate();
                ScrollToVerticalOffset(_verticalScrollValue * scrollableHeight * ZoomFactor);
            }
        }

        //TODO extract manipulation to own class
        private Point _lastMouseDown;
        private Point _secondPointDown;
        private Point _lastMouseDownCenter;
        private bool _moveBarrierCrossed;
        private bool _manipulationInProgress;
        private double _originalZoom = 1;

        private void ViewportPreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left)
            {
                _moveBarrierCrossed = false;
                _lastMouseDown = e.GetPosition(_viewport);
                _lastMouseDownCenter = _sketchPanel.PointFromScreen(
                    _viewport.PointToScreen(new Point(_viewport.ActualWidth * 0.5, _viewport.ActualHeight * 0.5)));
            }
        }

        private void ViewportMouseMove(object sender, MouseEventArgs e)
        {
            if (Mouse.LeftButton == MouseButtonState.Pressed && !_manipulationInProgress)
            {
                var currentPoint = e.GetPosition(_viewport);
                var dx = (_lastMouseDown.X - currentPoint.X) / ZoomFactor;
                var dy = (_lastMouseDown.Y - currentPoint.Y) / ZoomFactor;
                if (!_moveBarrierCrossed)
                {
                    if (dx * dx + dy * dy > 200)
                    {
                        _moveBarrierCrossed = true;
                    }
                }
                if (_moveBarrierCrossed)
                {
                    CenterTo(_lastMouseDownCenter.X + dx, _lastMouseDownCenter.Y + dy);
                }
            }
        }

        protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
        {
            if (_viewport != null && _viewport.IsVisible)
            {
                var boundingArea = GetGraphBoundingBox();
                if ((Keyboard.Modifiers & ModifierKeys.Control) != 0
                    || Mouse.RightButton == MouseButtonState.Pressed)
                {
                    ZoomByMouseWheel(e);
                }
                else if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0
                    || Mouse.LeftButton == MouseButtonState.Pressed)
                {
                    ScrollByMouseWheelHorizontal(e, boundingArea);
                }
                else
                {
                    ScrollByMouseWheelVertical(e, boundingArea);
                }
            }
            e.Handled = true;
        }

        private void ScrollByMouseWheelVertical(MouseWheelEventArgs e, ComputationalRectangle boundingArea)
        {
            double scrollableHeight = GetScrollableHeight(boundingArea);
            //scroll by 60 device pixels
            double delta = (60.0 / scrollableHeight) / ZoomFactor;
            if (e.Delta > 0)
            {
                if (_verticalScrollValue > ScrollbarMinimum)
                {
                    _verticalScrollValue = Math.Max(ScrollbarMinimum, _verticalScrollValue - delta);
                }
            }
            else
            {
                if (_verticalScrollValue < ScrollbarMaximum)
                {
                    _verticalScrollValue = Math.Min(ScrollbarMaximum, _verticalScrollValue + delta);
                }
            }
            if (double.IsNaN(_verticalScrollValue))
            {
                return;
            }
            _verticalScrollBar.Value = _verticalScrollValue;
            ScrollToVerticalOffset(_verticalScrollValue * scrollableHeight * ZoomFactor);
        }

        private void ScrollByMouseWheelHorizontal(MouseWheelEventArgs e, ComputationalRectangle boundingArea)
        {
            double scrollableWidth = GetScrollableWidth(boundingArea);
            //scroll by 60 device pixels
            double delta = (60.0 / scrollableWidth) / ZoomFactor;
            if (e.Delta < 0)
            {
                if (_horizontalScrollValue > ScrollbarMinimum)
                {
                    _horizontalScrollValue = Math.Max(ScrollbarMinimum, _horizontalScrollValue - delta);
                }
            }
            else
            {
                if (_horizontalScrollValue < ScrollbarMaximum)
                {
                    _horizontalScrollValue = Math.Min(ScrollbarMaximum, _horizontalScrollValue + delta);
                }
            }
            if (double.IsNaN(_verticalScrollValue))
            {
                return;
            }
            _horizontalScrollBar.Value = _horizontalScrollValue;
            ScrollToHorizontalOffset(_horizontalScrollValue * scrollableWidth * ZoomFactor);
        }

        protected override void OnPreviewTouchDown(TouchEventArgs e)
        {
            base.OnPreviewTouchDown(e);
            if (TouchesOver.Count() > 1)
            {
                var touches = TouchesOver.ToArray();
                _lastMouseDown = touches[0].GetTouchPoint(_viewport).Position;
                _secondPointDown = touches[1].GetTouchPoint(_viewport).Position;
                _manipulationInProgress = true;
                _originalZoom = ZoomFactor;
            }
        }

        protected override void OnPreviewTouchUp(TouchEventArgs e)
        {
            base.OnPreviewTouchUp(e);
            if (TouchesOver.Count() < 2)
            {
                _manipulationInProgress = false;
            }
        }

        protected override void OnTouchLeave(TouchEventArgs e)
        {
            base.OnTouchLeave(e);
            if (TouchesOver.Count() < 2)
            {
                _manipulationInProgress = false;
            }
        }

        protected override void OnTouchMove(TouchEventArgs e)
        {
            if (!_manipulationInProgress)
            {
                base.OnTouchMove(e);
                return;
            }
            var touches = TouchesOver.ToArray();
            var firstPoint = touches[0].GetTouchPoint(_viewport).Position;
            var secondPoint = touches[1].GetTouchPoint(_viewport).Position;

            var oldDifference = _lastMouseDown - _secondPointDown;
            var newDifference = firstPoint - secondPoint;

            var scaleFactor = Math.Sqrt(newDifference.X * newDifference.X + newDifference.Y * newDifference.Y) / Math.Sqrt(oldDifference.X * oldDifference.X + oldDifference.Y * oldDifference.Y);
            //var ptsketch = _sketchPanel.PointFromScreen(_viewport.PointToScreen(_lastMouseDown));
            //Point ptviewcenter = _sketchPanel.PointFromScreen(_viewport.PointToScreen(new Point(_viewport.ActualWidth * 0.5, _viewport.ActualHeight * 0.5)));
            //double oldZoom = ZoomFactor;
            ZoomFactor = _originalZoom * scaleFactor;
            //double newZoom = ZoomFactor;
            //Point ptdelta = new Point((ptsketch.X - ptviewcenter.X) * (oldZoom / newZoom), (ptsketch.Y - ptviewcenter.Y) * (oldZoom / newZoom));
            //CenterTo(ptsketch.X - ptdelta.X, ptsketch.Y - ptdelta.Y);

            e.Handled = true;
        }

        private void ZoomByMouseWheel(MouseWheelEventArgs e)
        {
            Point ptsketch = e.GetPosition(_sketchPanel);
            Point ptviewcenter =
                _sketchPanel.PointFromScreen(
                    _viewport.PointToScreen(new Point(_viewport.ActualWidth * 0.5, _viewport.ActualHeight * 0.5)));
            double oldZoom = ZoomFactor;
            if (e.Delta < 0)
            {
                ZoomOut();
            }
            else if (e.Delta > 0)
            {
                ZoomIn();
            }
            double newZoom = ZoomFactor;
            Point ptdelta = new Point((ptsketch.X - ptviewcenter.X) * (oldZoom / newZoom),
                                      (ptsketch.Y - ptviewcenter.Y) * (oldZoom / newZoom));
            CenterTo(ptsketch.X - ptdelta.X, ptsketch.Y - ptdelta.Y);
        }

        public void ZoomIn()
        {
            ZoomFactor *= Math.Pow(2.0, 0.25);
        }

        public void ZoomOut()
        {
            ZoomFactor /= Math.Pow(2.0, 0.25);
        }

        private void ScrollBarVerticalScroll(object sender, ScrollEventArgs e)
        {
            _verticalScrollValue = e.NewValue;
            var boundingArea = GetGraphBoundingBox();
            double scrollableHeight = GetScrollableHeight(boundingArea);
            ScrollToVerticalOffset(e.NewValue * scrollableHeight * ZoomFactor);
        }

        private void ScrollBarHorizontalScroll(object sender, ScrollEventArgs e)
        {
            _horizontalScrollValue = e.NewValue;
            var boundingArea = GetGraphBoundingBox();
            double scrollableWidth = GetScrollableWidth(boundingArea);
            ScrollToHorizontalOffset(e.NewValue * scrollableWidth * ZoomFactor);
        }

        private double GetScrollableHeight(ComputationalRectangle boundingArea)
        {
            return 0.5 * ((boundingArea.B - boundingArea.T) + _viewport.ActualHeight / ZoomFactor);
        }

        private double GetScrollableWidth(ComputationalRectangle boundingArea)
        {
            return 0.5 * ((boundingArea.R - boundingArea.L) + _viewport.ActualWidth / ZoomFactor);
        }

        private void ApplyTranslateTranform(double verticalOffset, double horizontalOffset)
        {
            Matrix oldmatrix = _transformBackup.Matrix;
            Matrix newmatrix = new Matrix(oldmatrix.M11,
                oldmatrix.M12, oldmatrix.M21, oldmatrix.M22, oldmatrix.OffsetX - horizontalOffset, oldmatrix.OffsetY - verticalOffset);

            _sketchPanel.RenderTransform = new MatrixTransform
            {
                Matrix = newmatrix
            };
        }

        private void ScrollToVerticalOffset(double p)
        {
            _verticalOffset = p;
            ApplyTranslateTranform(_verticalOffset, _horizontalOffset);
            UpdateViewport();
        }

        private void ScrollToHorizontalOffset(double p)
        {
            _horizontalOffset = p;
            ApplyTranslateTranform(_verticalOffset, _horizontalOffset);
            UpdateViewport();
        }

        internal ComputationalRectangle GetGraphBoundingBox()
        {
            if (_hilbertTree != null)
            {
                if (_hilbertTree.GetRoot() != null)
                {
                    return _hilbertTree.GetRoot().BoundingRegion;
                }
            }
            return new ComputationalRectangle(0, 0, 500, 500);
        }

        public void Arrange()
        {
            if (Nodes.Count == 0) return;
            foreach (Node n in _nodes.OfType<Node>())
            {
                n.SuppressPropertyChanged = true;
            }
            foreach (var transition in _transitions)
            {
                //transition.ClearControlPoints();
            }
            foreach (var subgraph in _nodes.OfType<IGraph>())
            {
                if (subgraph.IsExpanded)
                {
                    subgraph.Arrange();
                }
            }
            if (Arrangement != null)
            {
                Arrangement.Arrange();
            }
            foreach (var n in _nodes.OfType<Node>())
            {
                n.SuppressPropertyChanged = false;
                n.RaisePropertyChanged("CenterX");
                n.RaisePropertyChanged("CenterY");
            }
            UpdateHilbertTree();
            if (_sketchPanel != null)
            {
                _sketchPanel.Children.Clear();
            }
            UpdateViewport();
        }

        public IArrangement Arrangement { get; set; }

        private void UpdateHilbertTree()
        {
            _hilbertTree.RemoveAll();
            foreach (Transition t in _transitions)
            {
                _hilbertTree.InsertNode(t);
            }
            double minX = double.MaxValue;
            double minY = double.MaxValue;
            for (int i = 0; i < _nodes.Count; i++)
            {
                INode n = _nodes[i];
                minX = Math.Min(minX, n.Left);
                minY = Math.Min(minY, n.Top);

                _hilbertTree.InsertNode(n);
            }
            UpdateRenderTransform();
            var boundingArea = GetGraphBoundingBox();

            CenterTo((boundingArea.L + boundingArea.R) * 0.5, (boundingArea.B + boundingArea.T) * 0.5);
        }

        private void UpdateViewport()
        {
            if (_skipUpdateObjectsInScopeCount != 0 || _viewport == null) return;
            GeometryWpf.Node root = _hilbertTree.GetRoot();
            if (root == null)
                return;
            var boundingArea = GetGraphBoundingBox();
            Point pt0 = new Point(0.5 * (boundingArea.L + boundingArea.R) - (0.5 * _viewport.ActualWidth - _horizontalOffset) / ZoomFactor,
                                  0.5 * (boundingArea.B + boundingArea.T) - (0.5 * _viewport.ActualHeight - _verticalOffset) / ZoomFactor);
            double w = (_viewport.ActualWidth) / ZoomFactor;
            double h = (_viewport.ActualHeight) / ZoomFactor;
            Point pt1 = new Point(pt0.X + w, pt0.Y + h);

            ComputationalShape viewport = new ComputationalRectangle(pt0.X,
                                                                     pt0.Y,
                                                                     pt1.X,
                                                                     pt1.Y);
            var objectsInScope = new HashSet<IHilbertTreeNode>();
            HilbertTree.RangeSearch(root, viewport, objectsInScope);
            var alreadyVisible = new HashSet<IHilbertTreeNode>();
            var children = _sketchPanel.Children;

            //remove objects that are no longer visible
            for (int i = children.Count - 1; i > -1; i--)
            {
                UIElement child = children[i];
                if (child is PresenterBase)
                {
                    IHilbertTreeNode master = ((PresenterBase)child).HilbertNode;
                    if (!objectsInScope.Contains(master))
                    {
                        children.RemoveAt(i);
                    }
                    else
                    {
                        alreadyVisible.Add(master);
                    }
                }
            }
            //add objects, which became visible
            foreach (IHilbertTreeNode obj in objectsInScope)
            {
                if (alreadyVisible.Contains(obj))
                {
                    continue;
                }
                if (obj is SubGraph)
                {
                    children.Add(new SubGraphPresenter()
                    {
                        Data = (SubGraph)obj
                    });
                }
                else if (obj is Node)
                {
                    children.Add(new NodePresenter
                    {
                        Data = (Node)obj
                    });
                }
                else if (obj is Transition)
                {
                    var edge = (Transition)obj;
                    children.Insert(0, new TransitionPresenter
                    {
                        Data = edge
                    });
                }
            }
        }
    }
}
